package ph.com.gs3.formalistics.services.synchronizers;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import ph.com.gs3.formalistics.global.constants.LoggingType;
import ph.com.gs3.formalistics.global.constants.UserUpdateOptions;
import ph.com.gs3.formalistics.global.utilities.logging.FLLogger;
import ph.com.gs3.formalistics.model.LocalDataAccessObject.DeleteFailedException;
import ph.com.gs3.formalistics.model.LocalDataAccessObject.UpdateFailedException;
import ph.com.gs3.formalistics.model.RemoteDataAccessObject.RDAOCommunicationException;
import ph.com.gs3.formalistics.model.facades.CommentsDataWriter;
import ph.com.gs3.formalistics.model.facades.DocumentsDataReader;
import ph.com.gs3.formalistics.model.facades.FormsDataReader;
import ph.com.gs3.formalistics.model.ldao.CommentsLDAO;
import ph.com.gs3.formalistics.model.rdao.CommentsRDAO;
import ph.com.gs3.formalistics.model.vo.application.APIResponse;
import ph.com.gs3.formalistics.model.vo.application.APIResponse.APIInvalidResponseException;
import ph.com.gs3.formalistics.model.vo.business.Comment;
import ph.com.gs3.formalistics.model.vo.business.User;
import ph.com.gs3.formalistics.model.vo.business.document.Document;
import ph.com.gs3.formalistics.model.vo.business.form.Form;
import ph.com.gs3.formalistics.model.vo.parsers.TextHtmlParser;
import ph.com.gs3.formalistics.model.vo.parsers.json.CommentJSONParser;
import ph.com.gs3.formalistics.services.synchronizers.exceptions.SynchronizationFailedException;
import android.content.Context;
import android.util.Log;

public class CommentsSynchronizer extends AbstractSynchronizer<Void> {

	public static final String TAG = CommentsSynchronizer.class.getSimpleName();
	public static LoggingType LOGGING_TYPE;

	protected UsersSynchronizer usersSynchronizer;

	protected CommentsLDAO commentsLDAO;
	protected CommentsRDAO commentsRDAO;

	protected CommentsDataWriter commentsDataWriter;

	protected DocumentsDataReader documentsDataReader;
	protected FormsDataReader formsDataReader;

	protected User user;

	public CommentsSynchronizer(Context context, User user) {
		super(TAG, LOGGING_TYPE == null ? LoggingType.DISABLED : LOGGING_TYPE);
		this.user = user;

		usersSynchronizer = new UsersSynchronizer(context, user);

		commentsLDAO = new CommentsLDAO(context);
		commentsRDAO = new CommentsRDAO(user.getCompany().getServer());

		commentsDataWriter = new CommentsDataWriter(context, user);

		documentsDataReader = new DocumentsDataReader(context);
		formsDataReader = new FormsDataReader(context, user);

	}

	@Override
	public Void synchronize() {

		List<Form> forms = formsDataReader.getCompanyForms();

		submitOutgoingComments();
		deleteCommentsForDeletion();
		deleteCommentsDeletedFromServer();

		for (Form form : forms) {

			try {
				downloadNewCommentsAndUsers(form);
			} catch (SynchronizationFailedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}

		}

		return null;
	}

	public void submitOutgoingComments() {

		List<Comment> comments = new ArrayList<>();

		commentsLDAO.open();
		comments = commentsLDAO.getAllOutgoingComments(user.getId());
		commentsLDAO.close();

		for (Comment comment : comments) {
			try {
				submitOutgoingComment(comment);
			} catch (SubmitCommentFailedException | CommentSaveFailedException e) {
				Log.e(TAG, "Failed to submit comment");
			}
		}

	}

	public Comment submitOutgoingComment(Comment comment) throws SubmitCommentFailedException,
	        CommentSaveFailedException {

		// Parse the comment text to HTML
		comment.setText(TextHtmlParser.stringToHTML(comment.getText()));

		// Get the form and document of the comment

		Document document = documentsDataReader.getDocument(comment.getDocumentId());
		Form form = formsDataReader.getForm(document.getFormId());

		// Submit the comment to the server
		APIResponse response;
		try {
			response = commentsRDAO.submitComment(comment.getText(), form.getWebId(),
			        document.getWebId());
		} catch (APIInvalidResponseException | RDAOCommunicationException e) {
			throw new SubmitCommentFailedException(e);
		}

		FLLogger.d(TAG, "" + response.getResults());

		if (!response.isOperationSuccessful()) {
			throw new SubmitCommentFailedException(response.getErrorMessage());
		}

		int commentId = comment.getId();

		JSONObject resultingCommentValues;
		String resultingCommentWebId;
		String resultingCommentDateCreated;

		try {
			resultingCommentValues = new JSONObject(response.getResults());
			resultingCommentWebId = resultingCommentValues.getString("id");
			resultingCommentDateCreated = resultingCommentValues.getString("date_created");
		} catch (JSONException e) {
			throw new SubmitCommentFailedException("Unable to interpret response from server. ", e);
		}

		Comment submittedComment = null;

		try {
			// Save the submitted comment to the local device database
			commentsLDAO.open();
			submittedComment = commentsLDAO.updateOutgoingDocument(commentId,
			        resultingCommentWebId, resultingCommentDateCreated);
		} catch (UpdateFailedException e) {
			throw new CommentSaveFailedException(e);
		} finally {
			commentsLDAO.close();
		}

		return submittedComment;

	}

	public void deleteCommentsForDeletion() {

		List<Comment> comments = new ArrayList<>();

		try {
			commentsLDAO.open();
			comments = commentsLDAO.getAllCommentsMarkedForDeletion(user.getId());
		} finally {
			commentsLDAO.close();
		}

		for (Comment comment : comments) {
			deleteCommentMarkedForDeletion(comment);
		}

	}

	public void deleteCommentMarkedForDeletion(Comment comment) {

		// delete the comment from the server
		APIResponse response;
		try {
			response = commentsRDAO.deleteComment(comment.getWebId());
		} catch (RDAOCommunicationException | APIInvalidResponseException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return;
		}

		// Permanently delete the comment in the database
		if (response.isOperationSuccessful()) {
			commentsLDAO.open();
			try {
				commentsLDAO.deleteComment(comment.getId());
			} catch (DeleteFailedException e) {
				// TODO Add logging capabilities here
				e.printStackTrace();
			} finally {
				commentsLDAO.close();
			}

		} else {
			// TODO: throw exception here instead
			FLLogger.w(TAG, "Failed to delete comments on server: " + response.getErrorMessage());
			// throw new
			// CommentDeleteFromServerFailedException(response.getErrorMessage());
		}

	}

	public void deleteCommentsDeletedFromServer() {

		commentsLDAO.open();
		String lastUpdateDate = commentsLDAO.getLatestCommentDateCreated(user.getId());
		commentsLDAO.close();

		if (lastUpdateDate == null) {
			FLLogger.d(TAG, "Skipping synch of comment deletion, no last update yet.");
			return;
		}

		APIResponse response;
		try {
			response = commentsRDAO.getDeletedComments(lastUpdateDate);
		} catch (RDAOCommunicationException | APIInvalidResponseException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return;
		}

		if (response.isOperationSuccessful()) {
			String rawResultString = response.getResults();

			commentsLDAO.open();

			JSONArray rawResultsJSONArray;
			try {
				rawResultsJSONArray = new JSONArray(rawResultString);
				int commentCount = rawResultsJSONArray.length();
				for (int i = 0; i < commentCount; i++) {

					Comment comment = CommentJSONParser.createFromJSON(rawResultsJSONArray
					        .getJSONObject(i));

					try {
						FLLogger.d(TAG, "Deleting comment " + comment.getWebId());
						commentsLDAO.deleteComment(comment.getWebId(), user.getCompany().getId());
						FLLogger.d(TAG, "Deleted comment " + comment.getWebId());
					} catch (DeleteFailedException e) {
						FLLogger.w(TAG, "Failed to delete comment " + comment.getWebId()
						        + ", it's probably already deleted here.");
					}

				}
			} catch (JSONException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}

			commentsLDAO.close();

		} else {
			// TODO: throw an excpetion here
			FLLogger.e(TAG,
			        "Failed to download comments from the server: " + response.getErrorMessage());
		}

	}

	/**
	 * Downloads comments that are created after the last update date specified and the
	 * list of the documents/requests passed to this method. The list of document id must
	 * belong to a single form, downloading comments from multiple documents that also
	 * belong to multiple forms is not yet supported by the API. If the comment's author
	 * is not yet existing in this device's local database, it will automatically be
	 * saved.
	 * 
	 * @param formId
	 *            The web id of the form of the list of documents.
	 * @param documentIdList
	 *            The list of web id of the documents which comments will be updated.
	 * @param lastUpdateDate
	 *            The date when the comments are last updated
	 * @param rangeFrom
	 *            Will fetch comments starting from this parameter.
	 * @param rangeTo
	 *            Will fetch comments up to this parameter.
	 * @return The list of comments successfully downloaded from the server.
	 * @throws DownloadCommentsFailedException
	 */
	protected List<Comment> downloadNewCommentsAndUsers(Form form)
	        throws SynchronizationFailedException {

		commentsLDAO.open();
		String lastUpdateDate = commentsLDAO.getLatestFormDocumentsCommentDateCreated(user.getId(),
		        form.getId());
		commentsLDAO.close();

		List<Comment> comments = new ArrayList<>();

		Map<String, String> documentIdAndWebId = documentsDataReader
		        .getFormDocumentIdAndWebIdPairList(form.getId());

		if (documentIdAndWebId.size() <= 0) {
			// No requests found under this form
			return comments;
		}

		String[] documents = documentsDataReader
		        .getDocumentWebIdArrayFromIdAndWebIdPairList(documentIdAndWebId);
		// Throws SynchronizationFailedException
		APIResponse response = getCommentUpdates(form.getWebId(), documents, lastUpdateDate, 0, 100);

		String commentsJSONString = response.getResults();
		JSONArray commentsJSONArray = null;

		try {
			commentsJSONArray = new JSONArray(commentsJSONString);
		} catch (JSONException e) {
			throw new SynchronizationFailedException(
			        "The server response is not a valid JSON array");
		}

		int commentCount = commentsJSONArray.length();
		for (int i = 0; i < commentCount; i++) {

			JSONObject commentJSON = null;
			try {
				commentJSON = commentsJSONArray.getJSONObject(i);
			} catch (JSONException e) {
				throw new SynchronizationFailedException(
				        "One or more of the comments inside the server response is not a valid JSON object");
			}

			Comment comment = null;
			try {
				comment = CommentJSONParser.createFromJSON(commentJSON);
			} catch (JSONException e) {
				throw new SynchronizationFailedException(
				        "One or more of the comments inside the server response is not a valid comment JSON object. Some fields may be missing.");
			}

			User commentAuthor = null;
			try {
				commentAuthor = saveOrIgnoreAuthorFromCommentJSON(commentJSON);
			} catch (JSONException e) {
				throw new SynchronizationFailedException(
				        "Unable to read one or more of the comment's author");
			}

			String documentWebId;
			try {
				documentWebId = CommentJSONParser.getCommentDocumentWebIdFromJSON(commentJSON);
			} catch (JSONException e) {
				throw new SynchronizationFailedException(
				        "Unable to get the document's web id from the comment.");
			}

			int documentId = Integer.parseInt(documentIdAndWebId.get(documentWebId));

			comment.setDocumentId(documentId);
			comment.setAuthor(commentAuthor);

			Comment savedComment = commentsDataWriter.saveOrUpdateComment(comment);
			log("Updated comment: " + savedComment.toString());

			comments.add(savedComment);

		}

		return comments;
	}

	// {{ Utility Methods

	protected User saveOrIgnoreAuthorFromCommentJSON(JSONObject commentJSON) throws JSONException {

		EnumSet<UserUpdateOptions> userUpdateOptions = EnumSet
		        .of(UserUpdateOptions.UPDATE_EXCEPT_IS_ACTIVE,
		                UserUpdateOptions.UPDATE_EXCEPT_PASSWORD);

		User commentAuthor = CommentJSONParser.createCommentAuthorFromJSON(commentJSON);
		commentAuthor.setCompany(user.getCompany());

		commentAuthor = usersSynchronizer.updateUser(commentAuthor, userUpdateOptions);

		return commentAuthor;

	}

	protected APIResponse getCommentUpdates(String formWebId, String[] documentIdList,
	        String lastUpdateDate, int rangeFrom, int rangeTo)
	        throws SynchronizationFailedException {

		APIResponse response;

		try {
			response = commentsRDAO.getFormDocumentsCommentUpdates(formWebId, documentIdList,
			        lastUpdateDate, rangeFrom, rangeTo);

			if (!response.isOperationSuccessful()) {
				throw new SynchronizationFailedException(response.getErrorMessage());
			}

		} catch (RDAOCommunicationException | APIInvalidResponseException e) {
			throw new SynchronizationFailedException(e);
		}

		return response;
	}

	// }}

	// =======================================================================================
	// Exceptions

	public static final class SubmitCommentFailedException extends Exception {

		private static final long serialVersionUID = 6365152760879425731L;

		public SubmitCommentFailedException(String message) {
			super(message);
		}

		public SubmitCommentFailedException(String message, Throwable t) {
			super(message, t);
		}

		public SubmitCommentFailedException(Throwable t) {
			super(t);
		}

	}

	public static final class CommentSaveFailedException extends Exception {

		private static final long serialVersionUID = -2594829809583940982L;

		public CommentSaveFailedException(String message) {
			super(message);
		}

		public CommentSaveFailedException(String message, Throwable t) {
			super(message, t);
		}

		public CommentSaveFailedException(Throwable t) {
			super(t);
		}

	}

}
